1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.assetmanager;
12 import hip.util.data_structures: Node;
13 import hip.util.reflection;
14 import hip.error.handler;
15 import hip.console.log : hiplog;
16 
17 
18 private string buildConstantsFromFolderTree(string code, Node!string node, int depth = 0)
19 {
20     import hip.util.path;
21     import hip.util.string;
22     if(node.hasChildren && node.data.extension == "")
23     {
24         code = "\t".repeat(depth)~"class " ~ node.data~ "\n"~"\t".repeat(depth)~"{\n";
25         foreach(child; node.children)
26         {
27             code~= "\t".repeat(depth)~buildConstantsFromFolderTree(code, child, depth+1)~"\n";
28         }
29         code~="\n"~"\t".repeat(depth)~"}\n";
30     }
31     else if(!node.hasChildren && node.data.extension != "")
32     {
33         string propName = node.data[0..$-(node.data.extension.length+1)];
34         return "\tpublic static enum "~propName~" = `"~node.buildPath~"`;";
35     }
36     return code;
37 }
38 
39 mixin template HipAssetsGenerateEnum(string filePath)
40 {
41     import hip.util.path;
42     mixin(buildConstantsFromFolderTree("", buildFolderTree(import(filePath).split('\n'))));
43 }
44 
45 
46 import hip.util.system;
47 import hip.concurrency.thread;
48 import hip.concurrency.mutex;
49 public import hip.assets.image;
50 public import hip.audio.clip;
51 public import hip.assets.texture;
52 public import hip.assets.tilemap;
53 public import hip.font.bmfont;
54 public import hip.font.ttf;
55 public import hip.assets.csv;
56 public import hip.assets.jsonc;
57 public import hip.data.ini;
58 public import hip.api.data.commons;
59 public import hip.assets.textureatlas;
60 public import hip.util.data_structures;
61 public import hip.api.data.font;
62 public import hip.api.input.inputmap;
63 public import hip.assets.inputmap;
64 
65 
66 
67 class HipAssetManager
68 {
69     import hip.config.opts;
70 
71     package __gshared HipWorkerPool workerPool;
72     __gshared float currentTime;
73 
74     /**
75      * Due to a bug in the D Runtime, I can't use TypeInfo over the dll boundaries.
76      * `is` is being used instead of opEquals
77      */
78     protected __gshared IHipAssetLoadTask delegate(string path, const(ubyte)[] extraData, string file = __FILE__, size_t line = __LINE__)[string] typedAssetFactory;
79     //Caching
80     protected __gshared HipAsset[string] assets;
81     protected __gshared IHipAssetLoadTask[string] loadCache;
82 
83     //Thread Communication
84     protected __gshared IHipAssetLoadTask[] loadQueue;
85     protected __gshared void delegate(HipAsset)[][IHipAssetLoadTask] completeHandlers;
86     protected __gshared void delegate()[] onEveryLoadFinished;
87 
88 
89     public static void initialize()
90     {
91         import hip.loaders.audio;
92         import hip.loaders.fonts;
93         import hip.loaders.image;
94         import hip.loaders.text;
95         import hip.loaders.texture_atlas;
96         import hip.loaders.texture;
97         import hip.loaders.tilemap;
98         import hip.loaders.tileset;
99 
100         workerPool = new HipWorkerPool(HIP_ASSETMANAGER_WORKER_POOL);
101         typedAssetFactory = [
102             typeid(IHipAudioClip).toString : (string path, const(ubyte)[] extraData, string f, size_t l)=> new HipAudioLoadTask(path,path, null, extraData, f, l),
103             typeid(IHipFont).toString : (string path, const(ubyte)[] extraData, string f, size_t l)
104             {
105                 import hip.util.path;
106                 hiplog("Trying to load the font ", path, "EXT: ", path.extension);
107                 switch(path.extension)
108                 {
109                     case "bmfont":
110                     case "fnt":
111                         return new HipBMFontLoadTask(path, path, null, 48, extraData, f, l);
112                     case "ttf":
113                     case "otf":
114                         return new HipTTFFontLoadTask(path, path, null, 48, extraData, f, l);
115                     default: return null;
116                 }
117             },
118             typeid(IImage).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipImageLoadTask(path,path,null, extraData, f,l),
119             typeid(string).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipFileLoadTask(path,path,null, extraData, f,l),
120             typeid(IHipIniFile).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipINILoadTask(path,path,null,extraData,f,l),
121             typeid(IHipCSV).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipCSVLoadTask(path,path,null, extraData, f,l),
122             typeid(IHipJSONC).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipJSONCLoadTask(path,path,null, extraData, f,l),
123             typeid(IHipTextureAtlas).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipTextureAtlasLoadTask(path,path,null, ":IGNORE", extraData, f,l),
124             typeid(IHipTexture).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipTextureLoadTask(path,path,null, extraData, f,l),
125             typeid(IHipTilemap).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipTilemapLoadTask(path,path,null, extraData, f,l),
126             typeid(IHipTileset).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipTilesetLoadTask(path,path,null, extraData, f,l),
127             typeid(IHipInputMap).toString :  (string path, const(ubyte)[] extraData, string f, size_t l) => new HipInputMapLoadTask(path,path,null, extraData, f,l),
128         ];
129 
130         typedAssetFactory[typeid(HipAudioClip).toString] = typedAssetFactory[typeid(IHipAudioClip).toString];
131         typedAssetFactory[typeid(HipFont).toString] = typedAssetFactory[typeid(IHipFont).toString];
132         typedAssetFactory[typeid(Image).toString] = typedAssetFactory[typeid(IImage).toString];
133         typedAssetFactory[typeid(HipINI).toString] = typedAssetFactory[typeid(IHipIniFile).toString];
134         typedAssetFactory[typeid(HipJSONC).toString] = typedAssetFactory[typeid(IHipJSONC).toString];
135         typedAssetFactory[typeid(HipTextureAtlas).toString] = typedAssetFactory[typeid(IHipTextureAtlas).toString];
136         typedAssetFactory[typeid(HipTexture).toString] = typedAssetFactory[typeid(IHipTexture).toString];
137         typedAssetFactory[typeid(HipTileset).toString] = typedAssetFactory[typeid(IHipTileset).toString];
138         typedAssetFactory[typeid(HipInputMap).toString] = typedAssetFactory[typeid(IHipInputMap).toString];
139 
140 
141     }
142 
143     static bool isAsync = HipConcurrency;
144 
145     static if(HipConcurrency)
146     {
147         import core.sync.mutex;
148     }
149 
150 
151     @ExportD static HipAsset getAsset(string name)
152     {
153         if(HipAsset* asset = name in assets)
154             return *asset;
155         return null;
156     }
157 
158     @ExportD static string getStringAsset(string name)
159     {
160         HipAsset asset = getAsset(name);
161         if(asset !is null)
162         {
163             HipFileAsset fA = cast(HipFileAsset)asset;
164             assert(fA !is null, "Asset fetched is not a file asset.");
165             return fA.getText;
166         }
167         else
168             return null;
169     }
170 
171     static pragma(inline, true) T get(T)(string name) {return cast(T)getAsset(name);}
172     static pragma(inline, true) T get(T : string)(string name) {return getStringAsset(name);}
173 
174     ///Returns whether asset manager is loading anything
175     @ExportD static bool isLoading(string file = __FILE__, uint line = __LINE__)
176     {
177         return loadQueue.length != 0;
178     }
179     ///Returns whether asset manager is loading anything
180     @ExportD static int getAssetsToLoadCount()
181     {
182         return cast(int)loadQueue.length;
183     }
184 
185     ///Stops the code from running and awaits asset manager to finish loading
186     @ExportD static void awaitLoad()
187     {
188         workerPool.await;
189         update();
190     }
191 
192     static void awaitTask(IHipAssetLoadTask task)
193     {
194         static if(HipConcurrency)
195         {
196             import hip.asset_manager.load_task;
197             import core.sync.semaphore;
198             HipAssetLoadTask lTask = cast(HipAssetLoadTask)task;
199             auto semaphore = new Semaphore(0);
200             lTask.worker.pushTask("Await Single", ()
201             {
202                 semaphore.notify;
203             });
204             semaphore.wait;
205             destroy(semaphore);
206             update();
207         }
208     }
209 
210     package static HipWorkerThread loadWorker(string taskName, void delegate() loadFn, void delegate(string taskName) onFinish = null, bool onMainThread = false)
211     {
212         //TODO: Make it don't use at all worker and threads.
213         //? Maybe it is not actually needed, as it can be handled by version(HipConcurrency)
214         return workerPool.pushTask(taskName, loadFn, onFinish, onMainThread);
215     }
216     @ExportD static void registerAsset(TypeInfo tID, IHipAssetLoadTask delegate(string path, const(ubyte)[] extraData, string file, size_t line) assetFactory)
217     {
218         if(tID.toString in typedAssetFactory)
219         {
220             ErrorHandler.showErrorMessage("Asset already registered:", tID.toString);
221             throw new Exception("Attempt to register twice the same type.");
222         }
223         typedAssetFactory[tID.toString] = assetFactory;
224     }
225 
226     @ExportD static IHipAssetLoadTask loadAsset(TypeInfo tID, string path, const(ubyte)[] extraData = null, string file = __FILE__, size_t line = __LINE__)
227     {
228         IHipAssetLoadTask* cached = path in loadCache;
229         if(cached)
230             return *cached;
231         auto assetFactory = tID.toString in typedAssetFactory;
232         if(!assetFactory)
233         {
234             import hip.util.string;
235             String s = String();
236             ErrorHandler.showErrorMessage("Asset type was not registered in AssetManager:", tID.toString);
237 
238             foreach(type, factory; typedAssetFactory)
239                 s~= "\n\t- "~type.toString;
240 
241             ErrorHandler.showErrorMessage("Registered Types: ", s.toString);
242 
243             throw new Exception("Please register the type first.");
244         }
245 
246         IHipAssetLoadTask task = (*assetFactory)(path, extraData,file, line);
247         loadQueue~= task;
248         return task;
249     }
250 
251     @ExportD static IHipTilemap createTilemap(uint width, uint height, uint tileWidth, uint tileHeight)
252     {
253         return new HipTilemap(width, height, tileWidth, tileHeight);
254     }
255     @ExportD static IHipTileset tilesetFromAtlas(IHipTextureAtlas atlas){return HipTileset.fromAtlas(cast(HipTextureAtlas)atlas);}
256     @ExportD static IHipTileset tilesetFromSpritesheet(Array2D_GC!IHipTextureRegion sp){return HipTileset.fromSpritesheet(sp);}
257 
258     static void addOnCompleteHandler(IHipAssetLoadTask task, void delegate(HipAsset) onComplete)
259     {
260         import hip.asset_manager.load_task;
261         if(task.asset !is null)
262             onComplete(task.asset);
263         else
264         {
265             hiplog("Added a complete handler for ", (cast(HipAssetLoadTask)task).name);
266             completeHandlers[cast(HipAssetLoadTask)task]~= onComplete;
267         }
268     }
269 
270     static void addOnLoadingFinish(void delegate() onFinish)
271     {
272         onEveryLoadFinished~= onFinish;
273     }
274 
275     /**
276     *   This function is responsible for calling worker's onTaskFinish on the main thread if it has one.
277     *   After that, it will execute any deferred task to the AssetManager, such as setting a HipSprite or HipFont asset.
278     */
279     static void update()
280     {
281         import hip.util.array:remove;
282         import hip.asset_manager.load_task;
283         workerPool.startWorking();
284         for(int i = 0; i < loadQueue.length; i++)
285         {
286             IHipAssetLoadTask task = loadQueue[i];
287             HipAssetLoadTask lTask = cast(HipAssetLoadTask)task;
288             task.update();
289             final switch(task.result) with(HipAssetResult)
290             {
291                 case cantLoad:
292                     ErrorHandler.showWarningMessage("Could not load task: "~lTask.name, " Error: "~ lTask.error);
293                     loadQueue.remove(task);
294                     break;
295                 case loading, waiting:
296                     break;
297                 case mainThreadLoading:
298                     break;
299                 case loaded:
300                     if(auto handlers = task in completeHandlers)
301                     {
302                         //Subject to a logger
303                         hiplog(lTask.name, " executing handlers");
304                         foreach(handler; *handlers)
305                         {
306                             handler(lTask._asset);
307                         }
308                         handlers.length = 0;
309                         completeHandlers.remove(task);
310                     }
311                     loadQueue.remove(task);
312                     // loadCache[task.]
313                     i--;
314                     break;
315             }
316         }
317         if(loadQueue.length == 0 && onEveryLoadFinished.length)
318         {
319             foreach(handler; onEveryLoadFinished)
320                 handler();
321             onEveryLoadFinished.length = 0;
322         }
323         workerPool.pollFinished();
324     }
325 
326     /**
327     *   Cleans everything up. Puts AssetManager in an invalid state
328     */
329     static void dispose()
330     {
331         import hip.error.handler;
332         workerPool.dispose();
333         foreach(HipAsset asset; assets.byValue)
334             asset.dispose();
335         destroy(assets);
336         destroy(loadQueue);
337         destroy(workerPool);
338     }
339 }